{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit DirectShowDVD;

interface

uses AHMTypes, DirectShowVideo, DirectShow9;

const
  DVD_GRAPH_FILENAME = 'DVD';

type
  TAHMDirectShowDVD = class(TAHMDirectShowVideo)
  private
    FDVDControl: IDVDControl2;
    FDVDInfo: IDVDInfo2;
    function HMSFToSeconds(TimeCode: TDVDHMSFTimeCode): LongInt;
    function SecondsToHMSF(Seconds: LongInt): TDVDHMSFTimeCode;
  protected
    function GetDuration: Longint; override;
    function GetPosition: Longint; override;
    function GetChapterCount: Integer; override;
    procedure CustomGraphConfiguration; override;
    procedure CustomGraphBuild; override;
    procedure CustomGraphCompletion; override;
    procedure CustomGraphShutdown; override;
  public
    procedure CheckEventQueue; override;
    function Seek(Position: Longint): Boolean; override;
    procedure FastForward;
    procedure Rewind;
    procedure NextChapter;
    procedure PreviousChapter;
    function MenuActive: Boolean;
    function MenuReturn: Boolean;
    function MenuExit: Boolean;
    function MenuSelect: Boolean; overload;
    function MenuSelect(X, Y: Integer): Boolean; overload;
    function MenuNavigate(Direction: TAHMDirection): Boolean; overload;
    function MenuNavigate(X, Y: Integer): Boolean; overload;
  end;

implementation

uses Windows, Types, SysUtils, ActiveX, ApplicationManager, DirectShowHelper,
     DirectXHelper, DirectShowEnumerator, Logging;

//----------------------------------------------------------------------------//

function TAHMDirectShowDVD.HMSFToSeconds(TimeCode: TDVDHMSFTimeCode): LongInt;
begin
  // Convert DVD HMSF format to number of seconds
  with TimeCode do
    Result := bSeconds + (bMinutes * 60) + (bHours * 3600);
end;

function TAHMDirectShowDVD.SecondsToHMSF(Seconds: LongInt): TDVDHMSFTimeCode;
begin
  // Convert number of seconds to DVD HMSF format
  Result.bHours := Seconds div 3600;
  Seconds := Seconds mod 3600;
  Result.bMinutes := Seconds div 60;
  Seconds := Seconds mod 60;
  Result.bSeconds := Seconds;
  Result.bFrames := 0;
end;

function TAHMDirectShowDVD.GetDuration: Longint;
var
  Duration: TDVDHMSFTimeCode;
  TimecodeFlags: Cardinal;
  hr: HRESULT;
begin
  // Get current duration of dvd stream
  Result := 0;

  if FDVDInfo <> nil then
  begin
    // Timecode flags not used, but indicates intended DVD fps playback
    hr := FDVDInfo.GetTotalTitleTime(Duration, TimecodeFlags);
    if (hr = S_OK) or (hr = VFW_S_DVD_NON_ONE_SEQUENTIAL) then
      Result := HMSFToSeconds(Duration)
    else if hr <> VFW_E_DVD_INVALIDDOMAIN then
      LogWarn('Failed to get current DirectShow DVD duration: ' + DSGetErrorString9(hr));
  end;
end;

function TAHMDirectShowDVD.GetPosition: Longint;
var
  Location: TDVDPlayBackLocation2;
  hr: HRESULT;
begin
  // Get current position of dvd stream
  Result := 0;

  if FDVDInfo <> nil then
  begin
    hr := FDVDInfo.GetCurrentLocation(Location);
    if hr = S_OK then
      Result := HMSFToSeconds(Location.TimeCode)
    else if hr <> VFW_E_DVD_INVALIDDOMAIN then
      LogWarn('Failed to get current DirectShow DVD position: ' + DSGetErrorString9(hr));
  end;
end;

function TAHMDirectShowDVD.GetChapterCount: Integer;
var
  Location: TDVDPlayBackLocation2;
  Count: ULONG;
  hr: HRESULT;
begin
  // Set default result - no chapters
  Result := 0;

  if FDVDInfo <> nil then
  begin
    // Get current title number
    hr := FDVDInfo.GetCurrentLocation(Location);
    if hr = S_OK then
    begin
      // Get number of chapters for current title
      hr := FDVDInfo.GetNumberOfChapters(Location.TitleNum, Count);
      if hr = S_OK then
        Result := Count;
    end;

    if hr <> VFW_E_DVD_INVALIDDOMAIN then
      LogWarn('Failed to get current DirectShow DVD chapter count: ' + DSGetErrorString9(hr));
  end;
end;

procedure TAHMDirectShowDVD.CustomGraphConfiguration;
var
  FilterName: String;
  Enumerator: TAHMDirectShowEnumerator;
  DVDAudio, DVDVideo: IBaseFilter;
begin
  // Add standard audio renderer & VMR9 filters
  inherited;

  // Add custom MPEG2 audio & video decoders to graph
  Enumerator := TAHMDirectShowEnumerator.Create;
  try
    // Get custom DVD audio filter from preferences
    FilterName := ApplicationManagerInstance.PreferencesManager.DVDAudioFilter;
    if FilterName <> '' then
    begin
      DVDAudio := Enumerator.CreateDVDAudioFilter(FilterName);
      if (DVDAudio <> nil) and FGraph.AddFilter(DVDAudio, FilterName) then
        LogInfo('Added custom DVD audio filter to graph: ' + FilterName)
      else
        LogWarn('Failed to add custom DVD audio filter to graph: ' + FilterName);
    end;

    // Get custom DVD video filter from preferences
    FilterName := ApplicationManagerInstance.PreferencesManager.DVDVideoFilter;
    if FilterName <> '' then
    begin
      DVDVideo := Enumerator.CreateDVDVideoFilter(FilterName);
      if (DVDVideo <> nil) and FGraph.AddFilter(DVDVideo, FilterName) then
        LogInfo('Added custom DVD video filter to graph: ' + FilterName)
      else
        LogWarn('Failed to add custom DVD video filter to graph: ' + FilterName);
    end;
  finally
    Enumerator.Free;
  end;
end;

procedure TAHMDirectShowDVD.CustomGraphBuild;
var
  DVDNavigator: IBaseFilter;
  VideoPin, AudioPin, SubPicPin: IPin;
begin
  LogInfo('Building custom DirectShow graph for rendering DVD');

  // First, we'll need a DVDNavigator to handle DVD source media files
  DVDNavigator := FGraph.CreateFilter(CLSID_DVDNavigator);
  if DVDNavigator = nil then
    raise EDirectShowException.Create('Failed to create DirectShow DVDNavigator instance');
  if not FGraph.AddFilter(DVDNavigator, 'DVDNavigator') then
    raise EDirectShowException.Create('Failed to add DVDNavigator in DirectShow DVD graph');

  // Locate output pins for each stream type (audio, video and subtitles)
  VideoPin := FGraph.FindPinByName(DVDNavigator, 'Video');
  AudioPin := FGraph.FindPinByName(DVDNavigator, 'AC3');
  SubPicPin := FGraph.FindPinByName(DVDNavigator, 'SubPicture');

  // Verify each pin and render it to complete graph connections
  if VideoPin = nil then
    raise EDirectShowException.Create('Failed to find DVDNavigator "Video" pin in DirectShow graph');
  if not FGraph.RenderPin(VideoPin) then
    raise EDirectShowException.Create('Failed to render DVDNavigator "Video" pin - check MPEG2 codecs');
  if AudioPin = nil then
    raise EDirectShowException.Create('Failed to find DVDNavigator "AC3" pin in DirectShow graph');
  if not FGraph.RenderPin(AudioPin) then
    raise EDirectShowException.Create('Failed to render DVDNavigator "AC3" pin - check MPEG2 codecs');
  if SubPicPin = nil then
    raise EDirectShowException.Create('Failed to find DVDNavigator "SubPicture" pin in DirectShow graph');
  if not FGraph.RenderPin(SubPicPin) then
    raise EDirectShowException.Create('Failed to render DVDNavigator "SubPicture" pin - check MPEG2 codecs');

  LogInfo('Finished building custom DirectShow graph for rendering DVD');

  // Save the graph for analysis later
  FGraph.SaveGraph(DVD_GRAPH_FILENAME);
end;

procedure TAHMDirectShowDVD.CustomGraphCompletion;
var
  DVDNavigator: IBaseFilter;
begin
  // Call inherited method to validate graph and obtain standard video interface
  inherited;

  // Query graph for DVD specific interfaces
  DVDNavigator := FGraph.FindFilter(CLSID_DVDNavigator);
  if DVDNavigator = nil then
    raise EDirectShowException.Create('Failed to find DVDNavigator filter in DirectShow DVD graph');
  if DVDNavigator.QueryInterface(IID_IDVDControl2, FDVDControl) <> S_OK then
    raise EDirectShowException.Create('Failed to obtain DirectShow DVD control interface');
  if DVDNavigator.QueryInterface(IID_IDVDInfo2, FDVDInfo) <> S_OK then
    raise EDirectShowException.Create('Failed to obtain DirectShow DVD info interface');
end;

procedure TAHMDirectShowDVD.CustomGraphShutdown;
begin
  // Call inherited method to remove VMR9 interfaces
  inherited;

  // Free DVD specific interfaces
  FDVDControl := nil;
  FDVDInfo := nil;
end;

procedure TAHMDirectShowDVD.CheckEventQueue;
var
  hr: HRESULT;
  EventCode, Param1, Param2: Integer;
begin
  if Assigned(FMediaEvent) then
  begin
    // Check for any events in our queue - read them all until queue empty
    while True do
    begin
      hr := FMediaEvent.GetEvent(EventCode, Param1, Param2, 0);
      if hr <> S_OK then Break;

      // Check for interesting event codes - we ignore most events for now
      case EventCode of
//TODO:        EC_BUFFERING_DATA: - can use this later, check param1 for 0/1 values
        EC_DVD_ERROR, EC_DVD_DISC_EJECTED: FPlaybackError := True; // Stop playing
        EC_COMPLETE, EC_DVD_PLAYBACK_STOPPED: FEndOfFile := True; // File complete, set EOF flag
        EC_DVD_CURRENT_TIME: begin end; // Current time - do nothing
        else
          LogInfo('DirectShow event code: ' + IntToStr(EventCode) + ' found in DVD queue');
      end;

      // Tell direct show to free any resources associated with this event
      FMediaEvent.FreeEventParams(EventCode, Param1, Param2);
    end;
  end;
end;

function TAHMDirectShowDVD.Seek(Position: Longint): Boolean;
var
  DVDPosition: TDVDHMSFTimeCode;
  DVDCommand: IDvdCmd;
  hr: HRESULT;
begin
  Result := False;

  if FDVDControl <> nil then
  begin
    // Convert passed position in seconds to dvd time format
    DVDPosition := SecondsToHMSF(Position);
    DVDCommand := nil;

    // Ask dvd to jump to specified time within current title. We specify
    // asynchronous completion, so the IDvdCmd blocking object is not req'd
    hr := FDVDControl.PlayAtTime(@DVDPosition, DVD_CMD_FLAG_None, DVDCommand);

    if hr = S_OK then
      Result := True
    else if hr <> VFW_E_DVD_OPERATION_INHIBITED then
      LogWarn('Failed to perform DVD seek operation using DirectShow: ' + DSGetErrorString9(hr));
  end;
end;

function TAHMDirectShowDVD.MenuActive: Boolean;
var
  hr: HRESULT;
  Domain: TDVDDomain;
begin
  // Set default result - menu not active
  Result := False;

  if FDVDInfo <> nil then
  begin
    // Get current DVD domain
    hr := FDVDInfo.GetCurrentDomain(Domain);
    if hr = S_OK then
      Result := (Domain = DVD_DOMAIN_VideoManagerMenu) or
                (Domain = DVD_DOMAIN_VideoTitleSetMenu)
    else
      LogWarn('Failed to get current DirectShow DVD domain: ' + DSGetErrorString9(hr));
  end;
end;

function TAHMDirectShowDVD.MenuReturn: Boolean;
var
  hr: HRESULT;
  Sync: IDVDCmd;
begin
  // Set default result - can't navigate to parent
  Result := False;

  if FDVDControl <> nil then
  begin
    hr := FDVDControl.ReturnFromSubMenu(DVD_CMD_FLAG_None, Sync);
    if hr <> S_OK then
      LogDebug('Failed to return from DVD submenu: ' + DSGetErrorString9(hr))
    else
      Result := True;
  end;
end;

function TAHMDirectShowDVD.MenuExit: Boolean;
var
  hr: HRESULT;
  Sync: IDVDCmd;
begin
  // Set default result - can't resume playing
  Result := False;

  if FDVDControl <> nil then
  begin
    hr := FDVDControl.ReturnFromSubMenu(DVD_CMD_FLAG_None, Sync);
    if hr <> S_OK then
      LogDebug('Failed to resume playing from DVD menu: ' + DSGetErrorString9(hr))
    else
      Result := True;
  end;
end;

function TAHMDirectShowDVD.MenuSelect: Boolean;
var
  hr: HRESULT;
begin
  // Set default result - can't select
  Result := False;

  if FDVDControl <> nil then
  begin
    hr := FDVDControl.ActivateButton;
    if hr <> S_OK then
      LogDebug('Failed to activate selected DVD menu button: ' + DSGetErrorString9(hr))
    else
      Result := True;
  end;
end;

function TAHMDirectShowDVD.MenuSelect(X, Y: Integer): Boolean;
var
  CursorPos: TPoint;
begin
  // Set default result - can't select
  Result := False;

  if FDVDControl <> nil then
  begin
    CursorPos := Point(X, Y);
    Result := (FDVDControl.ActivateAtPosition(CursorPos) = S_OK);
  end;
end;

function TAHMDirectShowDVD.MenuNavigate(Direction: TAHMDirection): Boolean;
var
  hr: HRESULT;
  DVDDirection: TDVDRelativeButton;
begin
  // Set default result - can't navigate
  Result := False;

  if FDVDControl <> nil then
  begin
    // Determine which direction to navigate menu in
    case Direction of
      diLeft: DVDDirection := DVD_Relative_Left;
      diRight: DVDDirection := DVD_Relative_Right;
      diDown: DVDDirection := DVD_Relative_Lower;
      else DVDDirection := DVD_Relative_Upper;
    end;

    hr := FDVDControl.SelectRelativeButton(DVDDirection);
    if hr <> S_OK then
    begin
      LogDebug('Failed to navigate DVD menu button: ' + DSGetErrorString9(hr));

      // Try to select the default button onscreen instead
      hr := FDVDControl.SelectButton(1);
      if hr <> S_OK then
        LogDebug('Failed to select default DVD menu button: ' + DSGetErrorString9(hr))
      else
        Result := True;
    end
    else
      Result := True;
  end;
end;

function TAHMDirectShowDVD.MenuNavigate(X, Y: Integer): Boolean;
var
  hr: HRESULT;
  CursorPos: TPoint;
//  ox, oy: integer;
begin
  // Set default result - can't navigate
  Result := False;

  if FDVDControl <> nil then
  begin
{
    ox := x; oy := y;
    Y := Y - FPresentTargetRect.Top;
    Y := Round(Y / (FPresentTargetRect.Bottom - FPresentTargetRect.Top) * FVideoHeight);
LogError('X=' + inttostr(ox) + ' Y=' + inttostr(oy) + ' t=' + inttostr(fpresenttargetrect.Top) +
         ' b=' + inttostr(fpresenttargetrect.Bottom) + ' h=' + inttostr(fpresenttargetrect.Bottom - fpresenttargetrect.Top) +
         ' newY=' + inttostr(y));
}
    CursorPos := Point(X, Y);
    hr := FDVDControl.SelectAtPosition(CursorPos);
    if hr <> S_OK then
      LogDebug('Failed to navigate DVD menu button: ' + DSGetErrorString9(hr))
    else
      Result := True;
  end;
end;

procedure TAHMDirectShowDVD.FastForward;
//var
//  Command: IDvdCmd;
begin
(*
  // Note - this is extremely flaky on most codecs... use with care
  FDVDControl.PlayForwards(2.0, 0, Command);
*)
end;

procedure TAHMDirectShowDVD.Rewind;
//var
//  Command: IDvdCmd;
begin
(*
  // Note - this is extremely flaky on most codecs... use with care
  FDVDControl.PlayBackwards(0.5, 0, Command);
*)
end;

procedure TAHMDirectShowDVD.NextChapter;
var
  DVDCommand: IDvdCmd;
  hr: HRESULT;
begin
  if FDVDControl <> nil then
  begin
    // Skip to next chapter
    DVDCommand := nil;
    hr := FDVDControl.PlayNextChapter(DVD_CMD_FLAG_None, DVDCommand);

    if (hr <> S_OK) and (hr <> VFW_E_DVD_OPERATION_INHIBITED) then
      LogWarn('Failed to perform DVD chapter skip operation using DirectShow: ' + DSGetErrorString9(hr));
  end;
end;

procedure TAHMDirectShowDVD.PreviousChapter;
var
  DVDCommand: IDvdCmd;
  hr: HRESULT;
begin
  if FDVDControl <> nil then
  begin
    // Skip to next chapter
    DVDCommand := nil;
    hr := FDVDControl.PlayPrevChapter(DVD_CMD_FLAG_None, DVDCommand);

    if (hr <> S_OK) and (hr <> VFW_E_DVD_OPERATION_INHIBITED) then
      LogWarn('Failed to perform DVD chapter skip operation using DirectShow: ' + DSGetErrorString9(hr));
  end;
end;


end.
